<?php
/*
 * Copyright (c) 2010, webvariants GbR, http://www.webvariants.de
 *
 * Diese Datei steht unter der MIT-Lizenz. Der Lizenztext befindet sich in der
 * beiliegenden LICENSE Datei und unter:
 *
 * http://www.opensource.org/licenses/mit-license.php
 * http://de.wikipedia.org/wiki/MIT-Lizenz
 */

/**
 * Interne API und REDAXO-Extensions
 *
 * Diese Klasse sammelt zu einem Großteil nur Extensions, um den URL-Cache
 * immer aktuell zu halten. Für gewöhnlich will niemand diese Methoden direkt
 * aufrufen.
 *
 * @author Christoph
 */
abstract class _WV24_RealURL2
{
	/**
	 * ART_ADDED: Artikel wurde hinzugefügt
	 *
	 * Diese Methode fügt dem Cache die URL für den neuen Artikel hinzu.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function artAdded($params)
	{
		WV24_RealURL2::generateCache();
	}
	
	/**
	 * ART_UPDATED: Artikel wurde umbenannt
	 *
	 * Diese Methode aktualisiert die URL eines Artikels im Cache.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function artUpdated($params)
	{
		self::generateCache((int) $params['id'], (int) $params['clang']);
	}
	
	/**
	 * ART_DELETED: Artikel wurde gelöscht
	 *
	 * Diese Methode löscht die URL eines Artikels aus dem Cache.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function artDeleted($params)
	{
		self::generateCache((int) $params['id'], (int) $params['clang']);
	}
	
	/**
	 * ART_META_FORM_SECTION: Fragment-Formular erzeugen
	 *
	 * Diese Methode erzeugt das Formular, um das Fragment für einen Artikel
	 * aufzunehmen.
	 *
	 * @param  array $params  die von REDAXO übergebenen Parameter
	 * @return string         das übergebene Subject mit dem angehängten Formularteil
	 */
	public static function artMetaFormSection($params)
	{
		$articleID = (int) $params['id'];
		$article   = OOArticle::getArticleById($articleID);
		$fragment  = WV24_RealURL2::getURLFragment($articleID, $params['clang']);
		
		ob_start();
		require _WV24_PATH.'templates/meta.phtml';
		$content = ob_get_clean();

		return $params['subject'].$content;
	}
	
	/**
	 * ART_META_UPDATED: Metadaten eines Artikels wurden aktualisiert
	 *
	 * Diese Methode aktualisiert die URL eines Artikels in der Datenbank.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function artMetaUpdated($params)
	{
		$articleID = (int) $params['id'];
		$clang     = (int) $params['clang'];
		$fragment  = rtrim(wv_post('realurl2_fragment', 'string'), '/');
		$sql       = WV_SQLEx::getInstance();
		$article   = OOArticle::getArticleById($articleID);
		$field     = $article->isStartPage() ? 'wv24_catpath_fragment' : 'wv24_path_fragment';
		
		// Datenbank aktualisieren
		
		$sql->queryEx(
			'UPDATE #_article SET '.$field.' = ? WHERE id = ? AND clang = ?',
			array($fragment, $articleID, $clang), '#_'
		);
		
		self::generateCache($articleID, $clang);
		
		// Den Kategoriecache müssen wir selbst leeren.
		
		if ($article->isStartPage()) {
			rex_deleteCacheArticle($articleID, $clang);
		}
	}
	
	/**
	 * CAT_ADDED: Kategorie wurde hinzugefügt
	 *
	 * Diese Methode fügt dem Cache die neue URL der Kategorie hinzu.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function catAdded($params)
	{
		WV24_RealURL2::generateCache();
	}
	
	/**
	 * CAT_UPDATED: Kategorie wurde umbenannt
	 *
	 * Diese Methode aktualisiert die URL einer Kategorie und aller in ihr
	 * enthaltenen Artikel.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function catUpdated($params)
	{
		$categoryID = (int) $params['id'];
		$clang      = (int) $params['clang'];
		$fragment   = rtrim(wv_post('realurl2_fragment', 'string'), '/');
		$sql        = WV_SQLEx::getInstance();
		
		// Datenbank aktualisieren
		
		$sql->queryEx(
			'UPDATE #_article SET wv24_catpath_fragment = ? WHERE id = ? AND clang = ?',
			array($fragment, $categoryID, $clang), '#_'
		);
		
		self::generateCache($categoryID, $clang);
	}
	
	/**
	 * CAT_DELETED: Kategorie wurde gelöscht
	 *
	 * Diese Methode entfernt die URL einer Kategorie aus dem Cache.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function catDeleted($params)
	{
		self::generateCache((int) $params['id'], (int) $params['clang']);
	}
	
	/**
	 * CAT_FORM_EDIT: Fragment-Formular erzeugen
	 *
	 * Diese Methode erzeugt das Formular, um das Fragment für eine Kategorie
	 * aufzunehmen.
	 *
	 * @param  array $params  die von REDAXO übergebenen Parameter
	 * @return string         das übergebene Subject mit dem angehängten Formularteil
	 */
	public static function catFormEdit($params)
	{
		$categoryID = (int) $params['id'];
		$fragment   = WV24_RealURL2::getURLFragment($categoryID, (int) $params['clang'], true);
		
		ob_start();
		require _WV24_PATH.'templates/category.phtml';
		$content = ob_get_clean();

		return $params['subject'].$content;
	}
	
	/**
	 * CLANG_ADDED: Sprache wurde hinzugefügt
	 *
	 * Diese Methode spiegelt die URLs der ersten Sprache in die neue Sprache.
	 * Außerdem werden die benötigten Global Settings angepasst.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function clangAdded($params)
	{
		global $REX;
		
		if (count($REX['CLANG']) == 2) {
			self::createLanguageSettings(0);
		}
		
		self::createLanguageSettings($params['id']);
		
		// Wenn die zweite Sprache hinzugefügt wurde, müssen alle URLs neu generiert werden.
		WV24_RealURL2::generateCache(count($REX['CLANG']) == 2);
	}
	
	/**
	 * CLANG_UPDATED: Sprache wurde umbenannt
	 *
	 * Diese Methode aktualisiert die URLs der umbenannten Sprache.
	 * Außerdem werden die benötigten Global Settings angepasst.
	 *
	 * Sollte ein Fehler auftreten, wird via rex_warning() eine Warnung
	 * ausgegeben.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function clangUpdated($params)
	{
		global $REX;
		
		$id  = (int) $params['id'];
		$sql = WV_SQLEx::getInstance();
		$sql->queryEx('DELETE FROM #_wv24_cache WHERE clang = ?', $id, '#_');
		WV24_RealURL2::generateCache();
		
		try {
			$setting = _WV8_Setting::getInstance('realurl2.languages', 'language'.$id);
			$setting->setTitle($REX['CLANG'][$id]);
			$setting->update();
		}
		catch (Exception $e) {
			print rex_warning('Es trat ein Fehler beim Aktualisieren der Global Setting für diese Sprache auf.');
		}
	}
	
	/**
	 * CLANG_DELETED: Sprache wurde gelöscht
	 *
	 * Diese Methode entfernt die URLs der gelöschten Sprache.
	 * Außerdem werden die benötigten Global Settings angepasst.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function clangDeleted($params)
	{
		global $REX;
		
		$id  = (int) $params['id'];
		$sql = WV_SQLEx::getInstance();
		
		if (count($REX['CLANG']) == 1) {
			$sql->queryEx('DELETE FROM #_wv24_cache WHERE 1', array(), '#_');
		}
		else {
			$sql->queryEx('DELETE FROM #_wv24_cache WHERE clang = ?', $id, '#_');
		}
		
		WV8_Settings::deleteIfExists('realurl2.languages', 'language'.$id);
		
		if (count($REX['CLANG']) == 1) {
			WV8_Settings::deleteIfExists('realurl2.languages', 'language0');
		}
	}
	
	/**
	 * WV8_SETTINGS_UPDATED: Global Settings wurden geändert
	 *
	 * Diese Methode aktualisiert die URLs aller Sprachen.
	 *
	 * @param array $params  die von Global Settings übergebenen Parameter
	 */
	public static function settingsUpdated($params)
	{
		WV24_RealURL2::generateCache(true);
	}
	
	/**
	 * ALL_GENERATED: Cache wurde geleert
	 *
	 * Diese Methode erzeugt den gesamten Cache neu.
	 *
	 * @param array $params  die von REDAXO übergebenen Parameter
	 */
	public static function cacheCleared($params)
	{
		WV24_RealURL2::clearCache();
		WV24_RealURL2::generateCache();
	}
	
	/**
	 * OUTPUT_FILTER: Rewriting der Special-URLs aus dem Cache
	 *
	 * Diese Methode läuft über die erzeugte Seite und ersetzt die von Modulen
	 * oder Templates generierten URL-Platzhalter durch die echten URLs.
	 *
	 * Zusätzlich wird diese Methode auch das <base>-Tag im <head> setzen.
	 *
	 * @param  array $params  die von REDAXO übergebenen Parameter
	 * @return string         der geänderte HTML-Code
	 */
	public static function outputFilter($params)
	{
		$html  = $params['subject'];
		$proto = self::isSecure() ? 'https' : 'http';
		$base  = $proto.'://'.$_SERVER['SERVER_NAME'].WV24_RealURL2::getBase(true);
		
		// <base> hinzufügen oder ändern
		
		if (strpos($html, '<base ') === false) {
			$html = preg_replace('#<head(.*?)>#i', '<head$1>'."\n\t".'<base href="'.$base.'" />', $html);
		}
		else {
			$html = preg_replace('#<base.*?>#i', '<base href="'.$base.'" />', $html);
		}
		
		// Links ersetzen (%%realurl2://ARTIKEL-CLANG/?params%%)
		
		preg_match_all('#%%realurl2://([0-9]+)-([0-9]+)/(.*?)%%#i', $html, $matches, PREG_SET_ORDER);
		
		foreach ($matches as $match) {
			$url  = WV24_RealURL2::getURL($match[1], $match[2], $match[3]);
			$html = str_replace($match[0], $url, $html);
		}
		
		return $html;
	}
	
	/**
	 * URL_REWRITE: Ersetzen des URL-Rewriting-Algorithmus
	 *
	 * Diese Methode stellt die Integration von realURL2 in REDAXO dar. Sie wird
	 * für jede vom CMS erzeugte URL aufgerufen und bietet die Möglichkeit,
	 * entsprechend einzugreifen.
	 *
	 * @param  array $params  die von REDAXO übergebenen Parameter
	 * @return string         die fertige URL für den Artikel
	 */
	public static function urlRewrite($params)
	{
		$articleID = (int) $params['id'];
		$clang     = (int) $params['clang'];
		$divider   = $params['divider'];
		$params    = $params['params'];
		
		if (self::isCacheRequest()) {
			$params = WV24_RealURL2::buildQuery($params, $divider);
			return '%%realurl2://'.$articleID.'-'.$clang.'/'.$params.'%%';
		}
		
		return WV24_RealURL2::getURL($articleID, $clang, $params, $divider);
	}
	
	/**
	 * Artikel aus dem aktuellen Request ermitteln
	 *
	 * Diese Methode wird direkt nach dem Einbinden des AddOns ausgeführt und
	 * versucht, die aufgerufene URL zu erkennen und die dazugehörigen
	 * GET-Parameter wiederherzustellen.
	 *
	 * Sollte keine URL gefunden werden, so wird der Extension-Point
	 * REALURL2_CLUELESS ausgeführt. Entsprechende Extensions sollten versuchen,
	 * die URL zu erkennen und bei Erfolg auf die von realURL2 erzeugte URL
	 * weiterleiten.
	 *
	 * Nach der Verarbeitung des Requests (auch bei nicht erkannten, solange
	 * die Extensions in REALURL2_CLUELESS nicht wegsterben, um weiterleiten)
	 * wird der Extension-Point REALURL2_URL_DECODED aufgerufen.
	 */
	public static function dispatchRequest()
	{
		global $REX;
		
		// Aufrufe, die direkt auf die index.php zeigen, können ggf. direkt
		// weitergeleitet werden. In diesem Fall ist die Artikel-ID bereits
		// bekannt.
		
		if (basename($_SERVER['REQUEST_URI']) == 'index.php') {
			$newURL = WV24_RealURL2::getURL($REX['ARTICLE_ID'], $REX['CUR_CLANG']);
			
			if (self::isSecure()) {
				$newURL = preg_replace('#^http://#i', 'https://', $newURL);
			}
			
			header('HTTP/1.1 301 Moved permanently');
			WV_Redaxo::redirect($newURL); // stirbt weg
		}
		
		$server   = $_SERVER['SERVER_NAME'];
		$uri      = $_SERVER['REQUEST_URI'];
		$protocol = self::isSecure() ? 'https' : 'http';
		
		// Eventuellen Querystring abschneiden
		
		if (strpos($uri, '?')) {
			$uri = substr($uri, 0, strpos($uri, '?'));
		}
		
		if (strpos($uri, '&')) {
			$uri = substr($uri, 0, strpos($uri, '&'));
		}
		
		// aus /index.[html|php|phtml|xhtml] nur / machen
		
		$origURI = 'http://'.$server.$uri;
		$uri     = preg_replace('#/index\.(html|php|phtml|xhtml)$#i', '/', $uri);
		$request = 'http://'.$server.$uri;
		
		// URL finden
		
		$data = WV24_RealURL2::decodeURL($request);
		
		// Wenn nichts gefunden wurde, prüfen wir, ob sich nur die Domain unterscheidet.
		
		if ($data === false) {
			$defaultServer = WV24_RealURL2::getDefaultDomain(false);
			
			if ($server != $defaultServer) {
				$request = str_replace('http://'.$server, 'http://'.$defaultServer, $request);
				$data    = WV24_RealURL2::decodeURL($request);
			}
		}
		
		// Migrations-Extensions dürfen jetzt versuchen, die URL bereits
		// vor uns zu erkennen und ggf. eine Weiterleitung anzustoßen.
		// Extensions sollen selbstständig wegsterben, wenn sie weiterleiten
		// möchten.
		
		if ($data === false) {
			rex_register_extension_point('REALURL2_CLUELESS', $request, array('original' => $origURI), true);
		}
		
		// Wenn nichts gefunden wurde, zum Startartikel "weiterleiten"
		
		if ($data === false) {
			header('HTTP/1.1 404 Not Found');
			
			$_GET['article_id'] = $REX['NOTFOUND_ARTICLE_ID'];
			$_GET['clang']      = 0;
			
			$REX['ARTICLE_ID'] = $_GET['article_id'];
			$REX['CUR_CLANG']  = $_GET['clang'];
			
			rex_register_extension_point('REALURL2_URL_DECODED', $data, array(), true);
			
			return;
		}
		
		// Falls die gefundene URL von der gegebenen abweicht (z.B. wegen einem entfernten
		// /index.html), leiten wir auf die richtige Version weiter.
		// Achtung: Dabei Protokoll nicht verändern!
		
		if ($data['url'] != $origURI) {
			$newURL = $data['url'];
			
			if (self::isSecure()) {
				$newURL = preg_replace('#^http://#i', 'https://', $newURL);
			}
			
			header('HTTP/1.1 301 Moved permanently');
			WV_Redaxo::redirect($newURL); // stirbt weg
		}
		
		// Wenn Daten gefunden wurden, GET-Parameter entsprechend setzen
		
		if (is_array($data)) {
			$_GET['article_id'] = $data['article_id'];
			$_GET['clang']      = $data['clang'];
			
			$REX['ARTICLE_ID'] = $data['article_id'];
			$REX['CUR_CLANG']  = $data['clang'];
			
			// Benutzerspezifische Parameter wiederherstellen
			
			if (!empty($data['params'])) {
				$params = json_decode($data['params'], true);
				$_GET   = array_merge($_GET, $params);
			}
			
			rex_register_extension_point('REALURL2_URL_DECODED', $data, array(), true);
		}
	}
	
	/**
	 * Global Settings erzeugen
	 *
	 * Diese Methode erzeugt die benötigten Einstellungen, um die einzelnen
	 * Sprachkürzel verwalten zu können. Eigentlich wäre das Aufgabe der
	 * Sprachverwaltung im CMS selber, aber bei REDAXO müssen wir uns eben selbst
	 * behelfen.
	 *
	 * @param int $onlyClangID  die ID, dessen Einstellung erzeugt werden soll, oder null für alle
	 */
	public static function createLanguageSettings($onlyClangID = null)
	{
		global $REX;
		
		// Wir verwenden keine mehrsprachige Einstellung, um die Möglichkeit,
		// nur einen Wert (einsprachiges Interface) einzugeben, zu vermeiden.
		
		$namespace = 'realurl2.languages';
		$helptext  = 'Dieses Fragment wird zwischen Basis und URL eingefügt, z. B. http://example.com/[FRAGMENT]/kategorie/artikel.html';
		$params    = '1|32|'.json_encode(array('width' => '60px'));
		$pagename  = 'realURL2';
		$group     = 'Sprach-Fragmente';
		
		foreach ($REX['CLANG'] as $clangID => $clangName) {
			if ($onlyClangID !== null && $clangID != $onlyClangID) {
				continue;
			}
			
			$name = 'language'.$clangID;
			
			WV8_Settings::deleteIfExists($namespace, $name);
			
			$setting = WV8_Settings::create($namespace, $name, $clangName, $helptext, 1, $params, false, $pagename, $group, false, false);
			
			// lngX ist das Standard-Fragment
			
			foreach ($REX['CLANG'] as $cID => $cName) {
				$setting->setValue('lng'.$clangID, $cID);
			}
			
			$setting->update();
		}
	}
	
	/**
	 * Auf HTTPS prüfen
	 *
	 * @return boolean  true, wenn es sich um eine HTTPS-Verbindung handelt, sonst false
	 */
	public static function isSecure()
	{
		return !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
	}
	
	/**
	 * Sprach-Fragment ermitteln
	 *
	 * Bei mehrsprachigen Seiten ist die erste virtuelle Kategorie von dem für
	 * die jeweilige Sprache Kürzel belegt, wie zum Beispiel in
	 * `http://example.com/de/kat1/artikel.html`.
	 *
	 * @param  int $clang  die gewünschte Sprache
	 * @return string      das ermittelte Sprach-Fragment (aus den Global Settings)
	 */
	public static function getLanguageFragment($clang = WV_Redaxo::CLANG_CURRENT)
	{
		$clang     = WV_Redaxo::clang($clang);
		$namespace = 'realurl2.languages';
		$name      = 'language'.$clang;
		
		if (!WV8_Settings::exists($namespace, $name)) {
			self::createLanguageSettings($clang);
		}
		
		return WV8_Settings::getValue($namespace, $name, 'lng'.$clang);
	}
	
	/**
	 * Prüfen, ob URL_REWRITE zum Cachen aufgerufen wurde
	 *
	 * Wenn der Extension-Point URL_REWRITE ausgeführt wird, muss realURL2
	 * erkennen, ob die zu erzeugende URL direkt ausgegeben (oder zum Beispiel in
	 * E-Mails verwendet werden soll) oder ob die URL in den Cache gelegt werden
	 * soll. Im letzteren Fall muss eine spezielle Markierung zurückgegeben
	 * werden, um später bei jedem Request im Output-Filter diese zu erkennen und
	 * durch die aktuelle Version der URL zu ersetzen.
	 *
	 * Diese Methode prüft dazu, ob im aktuellen Stack-Trace die Funktion
	 * `rex_generateArticleContent()` enthalten ist. Wenn ja, handelt es sich
	 * vermutlich um einen "Cache-Request".
	 *
	 * @return boolean  true, wenn es sich um einen Cache-Request handelt, sonst false
	 */
	protected static function isCacheRequest()
	{
		$stacktrace = debug_backtrace(false);
		array_shift($stacktrace);
		
		foreach ($stacktrace as $call) {
			if (empty($call['function'])) {
				continue;
			}
			
			if ($call['function'] == 'rex_generateArticleContent') {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Cache neu generieren
	 *
	 * Diese Methode generiert für einen Artikel den Cache neu, indem zuerst
	 * eventuell vorhandene URLs aus dem Cache entfernt und dann neu berechnet
	 * werden.
	 *
	 * @param int $articleID  die ID des Artikels
	 * @param int $clang      die Sprach-ID
	 */
	protected static function generateCache($articleID, $clang)
	{
		$articleID = (int) $articleID;
		$clang     = (int) $clang;
		$sql       = WV_SQLEx::getInstance();
		
		// Cache leeren: Alle Artikel, in deren Pfad dieser Artikel enthalten ist.
		
		$sql->queryEx(
			'DELETE FROM #_wv24_cache WHERE clang = ? AND (`path` LIKE "%|'.$articleID.'|%" OR article_id = ?)',
			array($clang, $articleID), '#_'
		);
		
		// Cache neu generieren
		
		WV24_RealURL2::generateCache();
	}
}
